package org.erikaredmark.monkeyshines.menu;
import java.awt.DisplayMode;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Point;
import java.awt.Toolkit;
import java.awt.image.BufferStrategy;
import java.awt.image.BufferedImage;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.erikaredmark.monkeyshines.Bonzo;
import org.erikaredmark.monkeyshines.GameConstants;
import org.erikaredmark.monkeyshines.GameWorldLogic;
import org.erikaredmark.monkeyshines.KeyBindings;
import org.erikaredmark.monkeyshines.KeyboardInput;
import org.erikaredmark.monkeyshines.World;
import org.erikaredmark.monkeyshines.animation.GracePeriodAnimation;
import org.erikaredmark.monkeyshines.global.SpecialSettings;
import org.erikaredmark.monkeyshines.screendraw.StandardSurface;
import org.erikaredmark.monkeyshines.util.GameEndCallback;
import com.google.common.base.Function;
/**
*
* Represents the game running in full-screen mode.
* <p/>
* This class goes through three states: Constructed and ready to play, playing, and playing over. After a game
* is finished using this object a new instance must be created to start another game.
*
* @author Erika Redmark
*
*/
public final class GameFullscreenWindow extends Frame {
private static final String CLASS_NAME = "org.erikaredmark.monkeyshines.menu.GameFullscreenWindow";
private static final Logger LOGGER = Logger.getLogger(CLASS_NAME);
private static final long serialVersionUID = 1L;
/**
*
* Creates the fullscreen window for the game, but critically does not actually make it visible and start the game
* until {@code start() } is called
*
* @param keys
* keyboard input device to register
*
* @param keyBindings
* a binding object that determines which keys on the keyboard map to which
* actions bonzo can take.
*
* @param gameOver
* callback that is called when the game is over, and this object is done. Technically, this is called
* after this object has been disposed
*
* @param world
* world to start a game for
*
*/
public GameFullscreenWindow(final KeyboardInput keys,
final KeyBindings keyBindings,
final GameEndCallback gameOver,
final World w) {
gameOverCallback = gameOver;
assert keys != null;
addKeyListener(keys);
GraphicsEnvironment env =
GraphicsEnvironment.getLocalGraphicsEnvironment();
this.mainScreen = env.getDefaultScreenDevice();
this.universe =
new GameWorldLogic(keys,
keyBindings,
w,
// Game over: set variable to stop loop
new GameEndCallback() {
@Override public void gameOverWin(World w) {
// Allow tally screen
gameOverPreparations(false);
gameOverCallback.gameOverWin(w);
}
@Override public void gameOverFail(World w) {
gameOverPreparations(true);
gameOverCallback.gameOverFail(w);
}
@Override public void gameOverEscape(World w) {
gameOverPreparations(true);
gameOverCallback.gameOverEscape(w);
}
},
// Each game tick, rerender volatile
new Runnable() {
@Override public void run() {
// Whilst surface isn't initialised yet, the timer hasn't
// started so this won't be called if surface is null.
renderScene();
}
},
activateGraceAnimation,
SpecialSettings.isThunderbird() );
this.surface = new StandardSurface(universe);
setUndecorated(true);
setIgnoreRepaint(true);
// set size is fine on just the frame since no extra will be taken for the title bar
// and window decoration.
setSize(640, 480);
setAlwaysOnTop(true);
}
/**
*
* Starts the game. This will open a window and set fullscreen, then start the game. The main
* game loop will handle rendering. Fullscreen will be turned off via game events.
* <p/>
* This will return instantly if fullscreen is not supported.
*
* @return
* {@code true} if the game has started, {@code false} if the hardware did not support fullscreen
* and thus the game couldn't even start.
*
*/
public boolean start() {
if (!(mainScreen.isFullScreenSupported() ) ) {
return false;
}
setVisible(true);
createBufferStrategy(2);
buffer = getBufferStrategy();
if (!(mainScreen.isFullScreenSupported() ) ) {
LOGGER.info(CLASS_NAME + ": Full screen not supported on this machine.");
}
mainScreen.setFullScreenWindow(this);
//can we change the display mode? If not, we'll just take the performance hit
if (mainScreen.isDisplayChangeSupported() ) {
LOGGER.info(CLASS_NAME + ": Display change is supported: going to 640x480 resolution");
DisplayMode[] modes = mainScreen.getDisplayModes();
for (DisplayMode mode : modes) {
// Find a 640x480 display mode for the standardSurface
if ( mode.getBitDepth() >= 24
&& mode.getWidth() == 640
&& mode.getHeight() == 480) {
mainScreen.setDisplayMode(mode);
// displayChanged = true;
break;
}
// Reaching here means we did not find a suitable resolution. This effectively means no
// resolution change.
LOGGER.info(CLASS_NAME + ": No suitable resolution found (was looking for 640x480 at a bit-depth of greater than or equal to 24 bits");
}
}
// Rid of that annoying cursor.
Toolkit toolkit = Toolkit.getDefaultToolkit();
BufferedImage inviso = new BufferedImage(1, 1, BufferedImage.TRANSLUCENT);
this.setCursor(toolkit.createCustomCursor(inviso, new Point(0, 0), "Inviso") );
// Cursor WILL be reset back to normal when fullscreen ends.
// Artifical delay: It takes time for the monitor to go into fullscreen mode.
// TODO some way of polling it until it does so instead of guessing?
try {
Thread.sleep(4500);
} catch (InterruptedException ex) {
LOGGER.log(Level.WARNING,
"Delay thread interrupted: Game may start before full-screen mode is entered. Issue caused by: " + ex.getMessage(),
ex);
}
// Finally, start the music, and set a timer for starting the game
universe.start(true);
return true;
}
@Override public void paint(Graphics g) {
System.err.println("Paint called on fullscreen");
}
// This is added to the 'game loop' via the callback per game tick. Each game tick is one
// scene re-render. Note that this makes game updating, rendering, and sycning single threaded.
// TODO volatile images just don't like rendering to BufferStrategy properly. Using bufferedImage
// until a fix is found.
private void renderScene() {
// stop renderScene from being called during the game over tick
if (gameOver) return;
assert surface != null;
do {
do {
Graphics2D g = (Graphics2D) buffer.getDrawGraphics();
try {
surface.renderDirect(g, !(universe.showingSplash() ) );
if (graceAnimation != null) graceAnimation.paint(g);
} finally {
g.dispose();
}
} while (buffer.contentsRestored() );
buffer.show();
} while (buffer.contentsLost() );
if ( graceAnimation != null
&& !(graceAnimation.update() ) ) {
universe.unfreeze();
graceAnimation = null;
}
}
/**
*
* This method basically disposes the fullscreen window, setting the device back to windowed mode and
* restoring all settings. At this point the object is effectively 'dead' and cannot be reused.
* <p/>
* If {@code endInstant} is {@code false}, this will display the score tally first.
* <p/>
* This does not call the gameOverCallback. It simply decides whether to show the tally screen first (which
* is shown before any callbacks are used)
*
*/
private void gameOverPreparations(boolean endInstant) {
gameOver = true;
if (!(endInstant) ) {
EndGameBonusAnimation.runOnVolatile(buffer,
universe.getWorld() );
}
mainScreen.setFullScreenWindow(null);
// Display seems to automatically fix itself when fullscreen is ended
// if (displayChanged) {
// mainScreen.setDisplayMode(oldDisplayMode);
// }
universe.dispose();
setVisible(false);
dispose();
}
/**
* Creates a new grace period animation object and freezes the game (not the music). Renderer will resume gameplay when
* the animation indicates it is finished.
*/
private final Function<Bonzo, Void> activateGraceAnimation = new Function<Bonzo, Void>() {
@Override public Void apply(Bonzo bonzo) {
// repainting will NOT actual run the world, just paint it to allow the animation to run.
universe.freeze(false, renderScenePtr);
graceAnimation = new GracePeriodAnimation(universe.getBonzo(), (int)((double)(GameConstants.FRAMES_PER_SECOND * 1.5)), 0, 80);
return null;
}
};
private Runnable renderScenePtr = new Runnable() { @Override public void run() { renderScene(); } };
private final StandardSurface surface;
private final GameEndCallback gameOverCallback;
// Initially and may be null. If non-null, will be played alongside basic game rendering.
private GracePeriodAnimation graceAnimation;
// Configuration information
private final GraphicsDevice mainScreen;
private final GameWorldLogic universe;
private boolean gameOver;
// State variables for drawing.
private BufferStrategy buffer;
}